NativeEngine: texture upload, cubemap loading, and readback improvements - #1808
NativeEngine: texture upload, cubemap loading, and readback improvements#1808bkaradzic-microsoft wants to merge 9 commits into
Conversation
There was a problem hiding this comment.
Pull request overview
This PR extends BabylonNative’s NativeEngine texture pipeline so Babylon.js texture upload/load/readback behaviors match WebGL more closely, including cubemap container loading, incremental uploads, cube-face readback, and raw 3D texture support.
Changes:
- Add
NativeEngine::updateTextureDataandupdateTextureDirectlyto support incremental texture uploads and Babylon.js loader “direct upload” sinks. - Enable single-file cubemap container loading (
.dds/.ktx/.ktx2) and compute diffuse-IBL spherical-harmonics from decoded top mips. - Add cube-face readback support to
readTexture, plus 3D raw texture creation/upload plumbing and shader-compiler texelFetch coordinate flip fixes.
Reviewed changes
Copilot reviewed 7 out of 7 changed files in this pull request and generated 3 comments.
Show a summary per file
| File | Description |
|---|---|
| Plugins/ShaderCompiler/Source/ShaderCompilerTraversers.cpp | Moves texelFetch coordinate flipping into an AST post-visit rewrite, avoiding WEBMIN-stripped integer multiply paths. |
| Plugins/ShaderCompiler/Source/ShaderCompilerCommon.cpp | Turns ProcessSamplerFlip into an identity passthrough since flipping is handled in the AST. |
| Plugins/NativeEngine/Source/NativeEngine.h | Adds new NativeEngine entrypoints for incremental updates, 3D raw textures, and direct upload sink. |
| Plugins/NativeEngine/Source/NativeEngine.cpp | Implements cubemap container loading + SH computation, updateTextureData, updateTextureDirectly, raw 3D textures, and cube-face readback routing. |
| Core/Graphics/Source/Texture.cpp | Adds 3D texture create/update and tracks cube/3D flags on Texture objects. |
| Apps/Playground/Scripts/config.json | Un-excludes the Test updateTextureData playground test from automatic testing. |
Three fixes from review on BabylonJS#1808: - UpdateTextureData: the bounds check allows `layer` up to 6*numLayers for cube textures (six faces per array layer), but the call site passed that value straight through as the bgfx *side* and hardcoded array layer 0. For a cube array that meant a side index above 5 and every update landing on layer 0. Decompose `layer` into (layer / 6, layer % 6) so it matches the range the bounds check actually admits. - ReadTexture: the face/layer index was forwarded to encoder->blit as srcZ with no upper bound, so an out-of-range value could drive an out-of-bounds read inside bgfx. Validate it against the texture's srcZ extent. Note this is deliberately not a flat 0-5 check: Babylon.js passes this same argument for 2D arrays as a slice index (see BaseTexture.readPixels, which takes the faceIndex branch for `isCube || is2DArray`), where values above 5 are legitimate. The bound is 6*numLayers for cube textures and numLayers otherwise, matching UpdateTextureData. - Texture::Create3D: drop a duplicated `m_is3D = false;` store left over from copy/paste before the correct `m_is3D = true;`. Revalidated: ran=301 passed=301 failed=0 (RelWithDebInfo, Win32, D3D11).
…armonics loadCubeTexture now accepts a single self-contained cubemap container (all six faces + mips), decoded via bimg::imageParse, and uploads sides 0-5 x mips. ComputeCubeSphericalPolynomial derives the diffuse-IBL spherical harmonics from the top-mip faces (port of CubeMapToSphericalPolynomialTools) and returns the polynomial coefficients to JS. This is done natively because the WebGL upload and cube-readback paths are unimplemented on native and .dds stores no SH. The 6 prefiltered-environment PBR validation tests this unblocks stay excluded here; they need the paired Babylon.js change to ship in the babylonjs dependency first. Pairs with BabylonJS/Babylon.js#18560 (native createCubeTexture dispatch for single-URL containers). Depends on a babylonjs dependency bump including it. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Copilot-Session: 60c2ec68-6de1-445d-9fc9-b699db737eae
…ureData) updateTextureData previously threw "not implemented" on Native. Implement it so sub-rectangle texture updates work. - Add NativeEngine::UpdateTextureData: upload the requested sub-rectangle via bgfx::updateTexture2D (Texture::Update2D). Validates the JS-controlled rect against the mip extents, sizes the copy with bgfx::calcTextureSize (no bimg dependency, so it also works in no-image-loading builds), and mirrors the vertical flip the base texture upload applies so the sub-rect lines up on top-left-origin backends (e.g. D3D11). - Re-enable the "Test updateTextureData" validation test. Pairs with the Babylon.js change (engine.name = "Native" so name-gated WebGL _gl access skips Native, plus the updateTextureData override). CI stays red until a babylonjs npm with that change is published and the dependency bumped. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Implement the native sink for Babylon's _uploadDataToTextureDirectly / _uploadCompressedDataToTextureDirectly so single-file container textures (.dds/.ktx/.ktx2, plus Basis/IES/HDR/EXR/TGA) load through the same JS texture loaders WebGL/WebGPU use. The loaders upload one (face, mip) at a time WebGL texImage2D-style; bgfx needs the whole texture allocated first, so the underlying texture is created lazily on the first upload. Validates JS-provided dimensions against maxTextureSize before uint16 narrowing and the payload size against bimg::imageGetSize, uses bgfx::copy for async-owned upload memory, and matches the existing loader flip conventions (skipping row-flips for compressed formats). Addresses BabylonJS#218 (paired with the Babylon.js single-file cubemap loader change). Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
…tureCube A cube created with bgfx::createTextureCube must be updated with bgfx::updateTextureCube (per-face side index), not updateTexture2D. Add an IsCube() flag (set in Texture::CreateCube, cleared in Create2D/Attach) and, in NativeEngine::UpdateTextureData, branch cubes to Texture::UpdateCube(0, side, mip, ...). Widen the layer bounds check to 6*numLayers for cubes (the JS side passes the face index in the layer arg). This is the C++ half of the HDR createRawCubeTexture fix. The IBL tests it unblocks stay excluded here: they also need the Babylon.js-side half, which is not in the pinned babylonjs dependency. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Copilot-Session: 60c2ec68-6de1-445d-9fc9-b699db737eae
PrepareImage passed block-compressed / unsupported formats straight to
bimg::imageGenerateMips, which only supports RGBA8/RGBA32F and returns NULL
otherwise. The NULL image was then dereferenced in LoadTextureFromImage,
crashing with an access violation. Tests 250/251/252 ("PBR shader code
coverage 1/2/3", snippets #QI7TL3#63/64/65) load a 256x256 BC1 texture with
generateMips=true and hit this.
- PrepareImage: convert any non-RGBA8/RGBA32F format before imageGenerateMips
(float/high-precision -> RGBA32F, everything else incl. BC1/DXT1 -> RGBA8),
with a null-check on the imageConvert result.
- LoadTexture: throw (routes to onError) instead of dereferencing a null image.
The crash is eliminated on all three. They stay excluded here: they also load
a single-file environment cubemap, which needs the Babylon.js-side change that
is not in the pinned babylonjs dependency.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Copilot-Session: 60c2ec68-6de1-445d-9fc9-b699db737eae
readTexture ignored the cube face and always read srcZ=0, and the JS
_readTexturePixels threw for any cube faceIndex. As a result
readPixels(face) returned null and ConvertCubeMapToSphericalPolynomial
crashed with "Cannot read properties of null" for tests that compute
diffuse-IBL spherical harmonics from a dynamically rendered cube
("Realtime Filtering", "Refraction local cube map PBR").
- readTexture now accepts an optional faceIndex (info[9], -1 = plain 2D).
A cube-face read always routes through the blit path with srcZ = face
(bgfx::readTexture cannot address an individual cube face).
Both tests stay excluded here: they also require Babylon.js-side changes
that are not in the pinned babylonjs dependency.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Copilot-Session: 60c2ec68-6de1-445d-9fc9-b699db737eae
Graphics::Texture gains Create3D/Update3D (bgfx createTexture3D/ updateTexture3D) and NativeEngine gains a loadRawTexture3D binding, giving Babylon.js createRawTexture3D/updateRawTexture3D real 3D volumes on Native. Fix the sampler3D shader compile that blocks HAL Lattice: the vertical texel-coordinate flip was applied by a preprocessor macro that forced every texelFetch coordinate through ivec2(...), so sampler3D fetches failed with 'no matching overloaded function'. Move that flip into the dimension-aware FlipSamplerCoordinates AST traverser (only 2-component integer coords are flipped; 3D/array left intact), cloning the sampler and lod operands so the injected textureSize() call does not alias the original texelFetch subtree (aliasing corrupted the AST and crashed unrelated async tests on dispose). The flip is built as ivec2(uv.x, textureSize(s, lod).y - 1 - uv.y), matching the expression the old macro expanded to. The tidier vector form uv * ivec2(1, -1) + ivec2(0, size.y - 1) must not be used: it emits SPIR-V OpIMul, which SPIRV-Cross drops entirely in the SPIRV_CROSS_WEBMIN configuration Babylon Native builds (the handler is compiled out to a release-mode no-op assert). The multiply then yields no HLSL/MSL expression and the whole shader fails to cross-compile with "Cannot resolve expression type" - which regressed "Gaussian Splatting Compressed ply SH", the only enabled test that texelFetches a usampler2D. Integer subtract and vector construction are both retained by that build. The texelFetch rewrite runs on post-visit because it references the coordinate subtree twice; rewriting on the way down would make the traverser descend into that subtree twice and double-flip any nested texture() call. HAL Lattice (idx 128) stays excluded: it additionally needs Babylon.js-side 3D texture support, which is not in the pinned babylonjs dependency. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Copilot-Session: 60c2ec68-6de1-445d-9fc9-b699db737eae
Three fixes from review on BabylonJS#1808: - UpdateTextureData: the bounds check allows `layer` up to 6*numLayers for cube textures (six faces per array layer), but the call site passed that value straight through as the bgfx *side* and hardcoded array layer 0. For a cube array that meant a side index above 5 and every update landing on layer 0. Decompose `layer` into (layer / 6, layer % 6) so it matches the range the bounds check actually admits. - ReadTexture: the face/layer index was forwarded to encoder->blit as srcZ with no upper bound, so an out-of-range value could drive an out-of-bounds read inside bgfx. Validate it against the texture's srcZ extent. Note this is deliberately not a flat 0-5 check: Babylon.js passes this same argument for 2D arrays as a slice index (see BaseTexture.readPixels, which takes the faceIndex branch for `isCube || is2DArray`), where values above 5 are legitimate. The bound is 6*numLayers for cube textures and numLayers otherwise, matching UpdateTextureData. - Texture::Create3D: drop a duplicated `m_is3D = false;` store left over from copy/paste before the correct `m_is3D = true;`. Revalidated: ran=301 passed=301 failed=0 (RelWithDebInfo, Win32, D3D11).
73b33cf to
5d8c2e1
Compare
bghgary
left a comment
There was a problem hiding this comment.
[Reviewed by Copilot on behalf of @bghgary]
Concerns inline.
Separately, the deferred cubemap un-exclusions point at a dead link. The description names Babylon.js#18560 as the prerequisite, but that PR was closed on 2026-06-17 without merging, and the work did not land elsewhere: nativeEngine.cubeTexture.pure.ts on master was last touched by #18441 (tree-shaking) and still throws Cannot load cubemap because 6 files were not defined for a single file. No open Babylon.js PR replaces it.
| TIntermTyped* lodClone{CloneLeaf(lod)}; | ||
| if (samplerClone == nullptr || lodClone == nullptr) | ||
| { | ||
| return coordinate; |
There was a problem hiding this comment.
[Reviewed by Copilot on behalf of @bghgary]
CloneLeaf returns nullptr for anything that is not a symbol or a constant, and the flip is then skipped silently. texelFetch(s, coord, someComputedLod) samples with un-flipped Y and produces a wrong image with no diagnostic. Worth throwing or falling back rather than returning the coordinate unchanged.
There was a problem hiding this comment.
Confirmed, and fixed in a3241c1 — CloneLeaf is gone, replaced by CloneExpression, a structural deep clone that handles symbols, constant unions, binary, unary and aggregate nodes. It copies each node's operator, type and source location verbatim rather than rebuilding through TIntermediate::add*, so no constant folding or type promotion can make the copy diverge from the original. Anything it genuinely cannot copy now throws std::runtime_error instead of returning nullptr, so we can never again silently emit an unflipped coordinate.
Two notes from checking how much this actually mattered:
I surveyed the 133 texelFetch call sites in the Babylon.js shaders. lod is almost always a constant (0, 1) or a bare symbol, so the old leaf-only clone was usually enough for it. But coordinates are frequently complex — ivec2(gl_FragCoord.xy), ivec2(vUV * texSize), coord + ivec2(1,1), ivec2(t0 % w, t0 / w) are all common. So the nullptr path was not an exotic corner; the reason it did not show up as failures is the second point below.
One behaviour I want to flag explicitly rather than have it found later: cloning an EOpFunctionCall aggregate duplicates a side-effecting call. That is not a regression — it is exactly what the ProcessSamplerFlip macro this code replaced already did, since it expanded the coordinate operand twice — but it is inherent to expressing the flip as ivec2(c.x, size.y - 1 - c.y) and worth knowing about.
| // coordinate.x and coordinate.y | ||
| TIntermTyped* coordinateX{m_intermediate->addIndex(EOpIndexDirect, coordinate, m_intermediate->addConstantUnion(0, loc), loc)}; | ||
| coordinateX->setType(intType); | ||
| TIntermTyped* coordinateY{m_intermediate->addIndex(EOpIndexDirect, coordinate, m_intermediate->addConstantUnion(1, loc), loc)}; |
There was a problem hiding this comment.
[Reviewed by Copilot on behalf of @bghgary]
sampler and lod are cloned because reusing a node would give it two parents, but coordinate is referenced twice right here — once for .x on the line above, once for .y here — and is not cloned. By the argument in the comment at the top of this function, that is the same aliasing: the original reference is replaced by the returned aggregate, but two new ones are created in its place.
Is the coordinate safe for a reason the sampler and lod are not, or should it be cloned too?
There was a problem hiding this comment.
You were right, and thank you for pushing on it — the answer to "was the coordinate safe for a different reason?" is no, it was not safe at all. Fixed in a3241c1: the coordinate is now cloned too, with the original node supplying one reference and the clone the other, so every node ends up with exactly one parent.
The post-visit ordering only masked one symptom (a nested texelFetch being flipped twice). The aliased DAG still went on to sampler splitting and SPIR-V generation, which is precisely the situation the comment right above cites as the reason for cloning sampler and lod. I have updated that comment so it now explains why all three operands are cloned instead of implying the coordinate is special.
For coverage, I extended ShaderCompilation.CompileComprehensiveGLSL rather than relying on "the render suite still passes". That distinction turned out to matter: I instrumented the traverser and ran the full 301-test validation suite, and it only hit three texelFetch flips, all of them with a bare symbol coordinate and a constant lod — i.e. the suite gave the new code zero coverage. The test now exercises a constructor, a binary expression, a nested constructor over a float expression, a nested constructor over integer binaries, built-in calls, and a non-constant lod, which cover the unary, binary and aggregate clone paths and a symbol lod. Verified by the same instrumentation that all of those shapes now reach CloneExpression.
While writing that test I hit two unrelated constructs that the WEBMIN builds reject — integer multiply (the OpIMul case already called out in this PR) and bitwise &. I kept them out of the test and left a comment saying why, so nobody re-adds them and blames the flip.
| m_ownsHandle = true; | ||
| m_width = width; | ||
| m_height = height; | ||
| m_depth = depth; |
There was a problem hiding this comment.
[Reviewed by Copilot on behalf of @bghgary]
m_depth is only ever assigned here, and Dispose() clears just the handle — so a Texture re-created as 2D or cube after having been 3D keeps the old depth and Depth() returns a stale value. The sibling Create* methods each reset m_is3D but none of them reset m_depth.
Each Create* hand-assigning its own subset of the metadata is what made the dead m_is3D store possible too; a shared reset would close both.
There was a problem hiding this comment.
Confirmed and fixed in a3241c1. m_depth was assigned only by Create3D, and Dispose() clears just the handle, so a Texture re-created as 2D or cube after having been 3D kept reporting the old depth.
I took the shared-reset route you suggested rather than adding one more hand-written assignment. There is now a private ResetMetadata() that returns every shape field to its default, and Create2D, Create3D, CreateCube and Attach all call it immediately after Dispose() before assigning the subset that applies to them. That also let me delete the scattered m_isCube = false; m_is3D = false; lines, which is what made the class of bug possible in the first place: any field a given path forgets is now defaulted rather than inherited from the previous, differently shaped texture.
| const bgfx::Memory* mem{bgfx::alloc(requiredSize)}; | ||
| if (flip) | ||
| { | ||
| const uint32_t rowBytes{requiredSize / height}; |
There was a problem hiding this comment.
[Reviewed by Copilot on behalf of @bghgary]
requiredSize / height is only a row stride for uncompressed formats. Measured with bgfx::calcTextureSize on this branch (Win32 D3D11, RelWithDebInfo):
| format | size | storageSize | requiredSize / height |
correct block-row stride |
|---|---|---|---|---|
| BC1 | 4x4 | 8 | 2 | 8 |
| BC1 | 16x16 | 128 | 8 | 32 |
| BC3 / BC7 | 4x4 | 16 | 4 | 16 |
| RGBA8 | 4x4 | 64 | 16 | 16 |
So for a 4x4 BC1 the loop makes 4 passes of 2 bytes over a single 8-byte block, reversing the [color0:2][color1:2][indices:4] layout into garbage; at 16x16 it reverses 16 8-byte chunks where the real layout is 4 block-rows of 32. Only the uncompressed row matches.
Reachable by default rather than in principle: bgfx::getCaps()->originBottomLeft is 0 on D3D11 (bgfx logs origin top left), so flip = originBottomLeft ? invertY : !invertY is true whenever invertY is left at its default.
bgfx::updateTexture2D also needs block-aligned x/y/width/height for compressed textures, which is not checked either. Rejecting compressed formats here would be enough, given the base upload path already handles them.
There was a problem hiding this comment.
Confirmed, and your arithmetic is right. I checked the block info in bimg (image.cpp:30, {4, 4, 4, 8, ...} for BC1 — a 4x4 block in 8 bytes): at 4x4 requiredSize / height gives 8/4 = 2 against a real block row of 8, and at 16x16 it gives 128/16 = 8 against a correct 32.
It is also reachable by default rather than theoretical: originBottomLeft is only ever set true in renderer_gl.cpp, so on D3D11 it is 0 and flip = !invertY is true whenever invertY is left false.
Fixed in a3241c1 by rejecting block-compressed formats outright, as you suggested, rather than fixing the stride. Fixing the stride would not be enough: mirroring block rows cannot flip a texture vertically without re-encoding the texel rows packed inside each block. BC1 happens to keep its 4 index bytes in row order, but BC3 alpha and BC7 do not, so a correct flip means a full decode/re-encode. bgfx also wants block-aligned x/y/width/height here, which the code was not checking either; rejecting the formats moots that too.
The base upload path (loadTexture → PrepareImage) already handles compressed data, so this is not a capability loss, and the error message says so. The check is texture->Format() < bgfx::TextureFormat::Unknown, which works because every block-compressed format sorts before Unknown in bgfx's enum (bgfx.h:222, "Compressed formats above"). That keeps it bimg-free, which matters here since this file has to build in configurations where bimg is not linked.
…tureData Clone whole texel coordinate expressions instead of leaves only. CloneLeaf handled symbols and constant unions and returned nullptr for everything else, and the caller treated nullptr as "skip the flip". Any texelFetch whose sampler or lod was not a bare leaf therefore sampled with an un-flipped Y and produced a wrong image with no diagnostic. Replace it with CloneExpression, a structural deep clone covering symbols, constant unions, binary, unary and aggregate nodes, which throws instead of silently skipping when it meets something it cannot copy. The coordinate itself was also referenced twice, for .x and .y, without being cloned. That is the same aliasing the surrounding comment cites as the reason for cloning the sampler and lod, so clone it too and let the original supply one reference and the clone the other, leaving every node with exactly one parent. Cover the new paths in the comprehensive GLSL compilation test with the coordinate shapes that actually occur in Babylon shaders: a constructor, a binary expression, a nested constructor over a float expression, a nested constructor over integer binaries, built-in calls, and a non-constant lod. These exercise the unary, binary and aggregate clone paths, none of which the old code could handle. Integer multiply, divide, modulo and bitwise operators are avoided in the test because glslang and SPIRV-Cross are built in their WEBMIN configurations here and reject them for unrelated reasons. Reset texture metadata in one place. m_depth was only ever assigned by Create3D, so a Texture re-created as 2D or cube after having been 3D kept reporting the old depth. Rather than add one more hand-written assignment to each Create*, give them a shared ResetMetadata() so a field that a given path does not set cannot survive from the previous, differently shaped texture. Reject block-compressed formats in updateTextureData. The vertical flip derived its row stride as requiredSize / height, which is only a row stride for uncompressed formats. For BC1 at 4x4 that yields 2 bytes against a real block row of 8. Even with the right stride, mirroring block rows cannot flip a texture vertically without re-encoding the texel rows packed inside each block, and bgfx additionally requires block-aligned coordinates here. Reject these formats with a clear error; the base upload path already handles compressed data, so nothing is lost. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Copilot-Session: 60c2ec68-6de1-445d-9fc9-b699db737eae
|
Thanks for the review — all four were real, and all four are fixed in a3241c1. Replies are on the individual threads; summary here.
Validation: full render suite 301/301 (no regressions), One side-finding worth its own issue, which I am not changing here. with no file, line, or reason — which is what every BN shader compile failure looks like today. I lost a fair amount of time to this while writing the test above; the failing constructs were only identifiable by bisecting the shader source. This is the same shape of problem as the |
There was a problem hiding this comment.
Pull request overview
Copilot reviewed 8 out of 9 changed files in this pull request and generated no new comments.
Suppressed comments (3)
Plugins/NativeEngine/Source/NativeEngine.cpp:1923
- Error message grammar: this is thrown when mipmaps are requested for a 3D texture; the wording should be plural ('3D textures') to read correctly.
throw Napi::Error::New(Env(), "Texture 3D currently do not support mipmaps.");
Plugins/NativeEngine/Source/NativeEngine.cpp:319
- PrepareImage can now return a null image (e.g., when imageConvert fails), but other call sites in this file still pass its return value directly to LoadTextureFromImage / LoadCubeTextureFromImages without a null guard. To avoid potential null dereferences, have PrepareImage fail loudly (throw) and also check the result of imageGenerateMips, so null never escapes from this helper.
if (image == nullptr)
{
return nullptr;
}
Plugins/NativeEngine/Source/NativeEngine.cpp:1928
- Error message: "invert Y" is ambiguous and the grammar is off. Consider referencing the parameter name (invertY) and clarifying it's a vertical flip.
throw Napi::Error::New(Env(), "Texture 3D currently do not support invert Y.");
|
|
||
| if (TIntermAggregate* aggregate = node->getAsAggregate()) | ||
| { | ||
| auto* clone = new TIntermAggregate{aggregate->getOp()}; |
There was a problem hiding this comment.
This deep clone regresses MacOS_Sanitizers: it fails on this commit and passes on both 5d8c2e1a and master aa244ec9.
intermediate.h:1707: runtime error: load of value 190, which is not a valid value for type 'bool'
isUserDefined() <- CloneExpression:1931 <- CloneExpression:1906 <- FlipVerticalTexelCoordinate:1846
glslang initializes userDefined in TIntermAggregate() but not in TIntermAggregate(TOperator), so clones built here carry it uninitialized (190 = 0xBE fill). Default-constructing and setting the operator afterwards fixes that:
| auto* clone = new TIntermAggregate{aggregate->getOp()}; | |
| auto* clone = new TIntermAggregate{}; | |
| clone->setOperator(aggregate->getOp()); |
The read at 1931 still needs handling — it dereferences the same member on the source aggregate, so copying it unconditionally would inherit the same garbage.
|
|
||
| if (image == nullptr) | ||
| { | ||
| return nullptr; |
There was a problem hiding this comment.
PrepareImage can now return nullptr, but only the loadTexture call site checks it (L1650). The other three pass the result straight into an unconditional dereference: L1712 into LoadTextureFromImage (L334), and L2175 / L2222 into LoadCubeTextureFromImages (L368). Guard those three, or have PrepareImage throw instead of returning nullptr.
The two other conversions in this function are unchecked as well: the sRGB imageConvert at L284 falls through to image->m_format at L299, and the imageGenerateMips result at L323 is returned without a check.
What
Fills in a set of gaps in
NativeEngine's texture upload / readback paths so that Babylon.js texture APIs that already work on WebGL behave the same on Native.Seven self-contained commits:
975b63d8.dds/.ktx/.ktx2cubemaps, including embedded spherical-harmonic coefficientsb1bc011fNativeEngine::updateTextureData43dedc29updateTextureDirectlytexture-loader sink9871bf8eUpdateTextureDatatobgfx::updateTextureCube138e457735f2cdadNativeEngine.readTextured4d8d438sampler3DtexelFetchcoordinate-flip fixEach commit builds and runs on its own; they're ordered so the plumbing lands before the callers.
Validation
Full Playground validation suite,
RelWithDebInfo, Win32, D3D11:No regressions against master's baseline of 300/300.
About
config.jsonThis PR un-excludes exactly one test —
Test updateTextureData— and that one is verified to pass against the stock npmbabylonjs9.15.0 thatApps/node_modulesresolves to.I want to flag this explicitly because it bit me: while developing this I had a locally-built Babylon.js fork in
Apps/node_modules(12.7 MBbabylon.max.js, same declared version9.15.0as the 8.6 MB npm build). Against that fork, 16 tests appeared to pass. Against stock npm, only 1 of the 16 actually does. The other 15 need Babylon.js-side changes that haven't landed yet:nativeEngine.cubeTexture.pure.tsonmasterstill throwsCannot load cubemap because 6 files were not definedfor a single file.CDF renderer is not supported by the current engineTEXTURE_3D/FLOATtests → require the raw-3D-texture constants to be plumbed through the engine capsThose un-exclusions are deliberately not in this PR and will follow once the corresponding Babylon.js work is released.
Note for anyone touching the shader compiler
d4d8d438includes a fix inShaderCompilerTraversers.cppthat is worth calling out, because the failure mode is nasty and invisible in the common configuration.BabylonNative builds SPIRV-Cross with
SPIRV_CROSS_WEBMIN(see the rootCMakeLists.txt;BABYLON_NATIVE_DISABLE_WEBMINturns it off). In that configuration a number of opcodes — includingOpIMul— are compiled out toSPIRV_CROSS_INVALID_CALL(), which is a bareassert(false). UnderNDEBUGthat is a no-op: the instruction is visited, no result id is set, and the failure surfaces much later at the first consumer of that id asCannot resolve expression type.— and sinceSPIRV_CROSS_THROWis also stripped tothrow CompilerError("")under WEBMIN, the message you actually get is empty.Concretely: emitting
coord * ivec2(1, -1)from a traverser produces a silently broken HLSL/MSL/Vulkan shader. The fix here computes the flip asivec2(coord.x, textureSize(s, lod).y - 1 - coord.y), which only needsOpCompositeExtract/OpCompositeConstruct/OpISub— all of which WEBMIN retains. Because that formulation references the coordinate subtree twice,EOpTextureFetchhandling also moved fromEvPreVisittoEvPostVisitso the traverser doesn't descend into the duplicated subtree and double-flip nestedtexture()calls.Short version: don't emit integer multiply from a shader-compiler traverser. I'll file this upstream against SPIRV-Cross separately — a stripped opcode should fail loudly rather than emit a broken shader.